iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
Vue.js

邊學邊做:Vue.js 實戰養成計畫系列 第 27

Day 27:星球資料艙 — 星球知識頁 (v-for + 動態路由)

  • 分享至 

  • xImage
  •  

我們今天要延續昨天的Final Mission — 5 日實作計畫,設計一個Galactic Explorer!
今日目標:展示宇宙星球資訊,能點選切換細節頁面。

今天會做出:

  • /planets:星球清單(v-for 渲染)
  • /planets/:id:星球詳細(動態路由 + <transition>淡入)
  • 一份 planets.json(含 8 大行星的基本常識)
    圖片請放在 public/img/(Vite 會原樣輸出),JSON 放在 src/data/

1) 放資料與圖片

a) 建立資料夾

public/img/            # 圖片放這
src/data/              # JSON 放這

b) src/data/planets.json
(可先用這份,之後想再補充也行)

[
  {
    "id": 1,
    "slug": "mercury",
    "name": "水星",
    "summary": "最接近太陽,溫差極大、沒有大氣保護。",
    "image": "/img/mercury.jpg",
    "facts": {
      "英文名": "Mercury",
      "類型": "類地行星",
      "半徑": "2,440 km",
      "公轉週期": "88 地球日",
      "自轉週期": "~58.6 地球日",
      "平均溫度": "-173 ~ 427°C",
      "衛星數": "0",
      "大氣": "極稀薄(幾乎沒有)",
      "小知識": "遍布隕石坑,白天酷熱、夜晚極冷。"
    }
  },
  {
    "id": 2,
    "slug": "venus",
    "name": "金星",
    "summary": "濃厚二氧化碳大氣與硫酸雲,溫室效應最強。",
    "image": "/img/venus.jpg",
    "facts": {
      "英文名": "Venus",
      "類型": "類地行星",
      "半徑": "6,052 km",
      "公轉週期": "225 地球日",
      "自轉週期": "~243 地球日(逆行)",
      "平均溫度": "~465°C",
      "衛星數": "0",
      "大氣": "以 CO₂ 為主,濃厚雲層",
      "小知識": "自轉極慢且方向與多數行星相反。"
    }
  },
  {
    "id": 3,
    "slug": "earth",
    "name": "地球",
    "summary": "液態海洋與可呼吸大氣,唯一已知有生命的行星。",
    "image": "/img/earth.jpg",
    "facts": {
      "英文名": "Earth",
      "類型": "類地行星",
      "半徑": "6,371 km",
      "公轉週期": "365.25 天",
      "自轉週期": "24 小時",
      "平均溫度": "~15°C",
      "衛星數": "1(月球)",
      "大氣": "氮氣、氧氣為主",
      "小知識": "表面 70% 是海洋。"
    }
  },
  {
    "id": 4,
    "slug": "mars",
    "name": "火星",
    "summary": "紅色沙塵世界,曾有液態水的跡象,殖民想像熱門。",
    "image": "/img/mars.jpg",
    "facts": {
      "英文名": "Mars",
      "類型": "類地行星",
      "半徑": "3,390 km",
      "公轉週期": "687 地球日",
      "自轉週期": "24.6 小時",
      "平均溫度": "-60°C",
      "衛星數": "2(火衛一、火衛二)",
      "大氣": "稀薄 CO₂",
      "小知識": "擁有太陽系最高的火山奧林帕斯山。"
    }
  },
  {
    "id": 5,
    "slug": "jupiter",
    "name": "木星",
    "summary": "太陽系最大行星,巨大的大紅斑風暴。",
    "image": "/img/jupiter.jpg",
    "facts": {
      "英文名": "Jupiter",
      "類型": "氣體巨行星",
      "半徑": "69,911 km",
      "公轉週期": "11.86 年",
      "自轉週期": "~10 小時",
      "平均溫度": "-110°C(雲頂)",
      "衛星數": "95+",
      "大氣": "氫、氦為主",
      "小知識": "伽利略衛星之一的歐羅巴可能有地下海洋。"
    }
  },
  {
    "id": 6,
    "slug": "saturn",
    "name": "土星",
    "summary": "最壯麗的行星環,冰與岩塊構成。",
    "image": "/img/saturn.jpg",
    "facts": {
      "英文名": "Saturn",
      "類型": "氣體巨行星",
      "半徑": "58,232 km",
      "公轉週期": "29.5 年",
      "自轉週期": "~10.7 小時",
      "平均溫度": "-140°C(雲頂)",
      "衛星數": "145+",
      "大氣": "氫、氦為主",
      "小知識": "土衛六泰坦擁有濃厚大氣與甲烷湖。"
    }
  },
  {
    "id": 7,
    "slug": "uranus",
    "name": "天王星",
    "summary": "自轉軸幾乎躺平,呈現側滾般的自轉。",
    "image": "/img/uranus.jpg",
    "facts": {
      "英文名": "Uranus",
      "類型": "冰巨行星",
      "半徑": "25,362 km",
      "公轉週期": "84 年",
      "自轉週期": "~17.2 小時(逆行)",
      "平均溫度": "-195°C(雲頂)",
      "衛星數": "27+",
      "大氣": "氫、氦、甲烷",
      "小知識": "藍綠色來自甲烷吸收紅光。"
    }
  },
  {
    "id": 8,
    "slug": "neptune",
    "name": "海王星",
    "summary": "最遠行星,強風與深藍色雲帶著名。",
    "image": "/img/neptune.jpg",
    "facts": {
      "英文名": "Neptune",
      "類型": "冰巨行星",
      "半徑": "24,622 km",
      "公轉週期": "164.8 年",
      "自轉週期": "~16 小時",
      "平均溫度": "-200°C(雲頂)",
      "衛星數": "14+",
      "大氣": "氫、氦、甲烷",
      "小知識": "擁有太陽系最快的急流風(> 2,000 km/h)。"
    }
  }
]

圖片檔名可先放占位圖(ex: 從網路抓、或自己放空白圖),只要檔名對上即可。

2) Router:新增詳細頁路由

src/router/index.js 增加一條動態路由(並建立對應檔案 PlanetDetail.vue):

import { createRouter, createWebHistory } from 'vue-router'

import Home from '../views/Home.vue'
import Planets from '../views/Planets.vue'
import Diary from '../views/Diary.vue'
import Scanner from '../views/Scanner.vue'
import NotFound from '../views/NotFound.vue'
import PlanetDetail from '../views/PlanetDetail.vue'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', name: 'home', component: Home },
    { path: '/planets', name: 'planets', component: Planets },
   { path: '/planets/:id', name: 'planet-detail', component: PlanetDetail, props: true },
    { path: '/diary', name: 'diary', component: Diary },
    { path: '/scanner', name: 'scanner', component: Scanner },
    { path: '/:pathMatch(.*)*', name: '404', component: NotFound }
  ],
  scrollBehavior: () => ({ top: 0 })
})

export default router

3) 清單頁 /planetssrc/views/Planets.vue

<template>
  <section>
    <h2>🗺️ 星球知識庫</h2>
    <p class="dim">點擊卡片前往詳情頁。</p>

    <div class="grid">
      <article v-for="p in planets" :key="p.id" class="card planet">
        <img :src="p.image" :alt="p.name" />
        <h3>{{ p.name }}</h3>
        <p class="sum">{{ p.summary }}</p>
        <el-button type="primary" @click="$router.push(`/planets/${p.id}`)">
          查看詳情
        </el-button>
      </article>
    </div>
  </section>
</template>

<script setup>
import planets from '../data/planets.json'
</script>

<style scoped>
.dim{ color: var(--text-dim); margin-bottom: 12px; }
.grid{
  display: grid; gap: 16px;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
.planet img{
  width: 100%; height: 140px; object-fit: cover; border-radius: 12px;
  margin-bottom: 8px;
}
.sum{ color: var(--text-dim); min-height: 44px; }
</style>

4) 詳細頁 /planets/:idsrc/views/PlanetDetail.vue

<template>
  <transition name="fade">
    <section v-if="planet" class="card detail">
      <header class="head">
        <img :src="planet.image" :alt="planet.name" />
        <div>
          <h2>{{ planet.name }}</h2>
          <p class="dim">{{ planet.summary }}</p>
          <el-button @click="$router.back()">← 返回列表</el-button>
        </div>
      </header>

      <h3>📘 基本資料</h3>
      <ul class="facts">
        <li v-for="(val, key) in planet.facts" :key="key">
          <span class="k">{{ key }}</span>
          <span class="v">{{ val }}</span>
        </li>
      </ul>
    </section>
  </transition>

  <section v-else class="card">
    <h2>找不到這顆星球</h2>
    <el-button type="primary" @click="$router.push('/planets')">回到列表</el-button>
  </section>
</template>

<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import data from '../data/planets.json'

const route = useRoute()
const currentId = computed(() => Number(route.params.id))
const planet = computed(() => data.find(p => p.id === currentId.value))
</script>

<style scoped>
.detail { padding: 20px; }
.head { display: grid; grid-template-columns: 220px 1fr; gap: 16px; align-items: center; }
.head img { width: 100%; height: 180px; object-fit: cover; border-radius: 12px; }
.dim{ color: var(--text-dim); margin: 6px 0 12px; }

.facts{
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: 8px;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.facts li{
  display: grid; grid-template-columns: 120px 1fr; gap: 8px;
  padding: 10px 12px; border:1px solid rgba(167,139,250,.2); border-radius: 12px;
  background: #0f172a;
}
.k{ color:#a78bfa; }
.v{ color:#e0e7ff; }

/* Transition 淡入 */
.fade-enter-active, .fade-leave-active { transition: opacity .35s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>

說明:

  • 進入詳情時由 <transition name="fade"> 控制淡入。
  • planet.facts 採「key-value」列表顯示,增加可讀性。
  • 找不到 ID(使用者手動改網址)就給友善頁面。

5) 圖片放哪?

把行星圖片放到 public/img/,檔名對應 JSON,我有上傳一些星球圖片,大家可以參考下載:https://github.com/jaina0831/planets.git

https://ithelp.ithome.com.tw/upload/images/20251009/20178644NoI3F3ZLUl.png
最後完成今天的目標會長這樣,大家一樣可以自己美編,完成獨一無二的Galactic Explorer,明天我們將會把 Day 23 的 localStorage 技巧搬進 /diary,做出可新增、勾選、刪除、永久保存的日誌(搭配 Element Plus 的表單/卡片美化)

參考資源
https://vuejs.org/guide
https://www.runoob.com/vue3


上一篇
Day 26:星際架構啟動 — 專案規劃與版面設計
下一篇
Day 28:宇宙觀察日誌 — localStorage 資料保存
系列文
邊學邊做:Vue.js 實戰養成計畫30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言